package com.lsa.skienna.challenge.chap2;

import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import junit.framework.TestCase;

import com.lsa.helpers.Pair;
import com.lsa.helpers.Utils;
import com.lsa.skienna.challenge.chap2.Hands.Hand;
import com.lsa.skienna.challenge.chap2.Hands.HandEstimator;
import com.lsa.skienna.challenge.chap2.Hands.HandEstimator.ConsecutiveRule;
import com.lsa.skienna.challenge.chap2.Hands.HandEstimator.FlashRule;
import com.lsa.skienna.challenge.chap2.Hands.HandEstimator.FourOfAKindRule;
import com.lsa.skienna.challenge.chap2.Hands.HandEstimator.FullHouseRule;
import com.lsa.skienna.challenge.chap2.Hands.HandEstimator.HandScorer;
import com.lsa.skienna.challenge.chap2.Hands.HandEstimator.OnePairRule;
import com.lsa.skienna.challenge.chap2.Hands.HandEstimator.ScoredRule;
import com.lsa.skienna.challenge.chap2.Hands.HandEstimator.StraightFlushRule;
import com.lsa.skienna.challenge.chap2.Hands.HandEstimator.StraightRule;
import com.lsa.skienna.challenge.chap2.Hands.HandEstimator.ThreeOfAKindRule;
import com.lsa.skienna.challenge.chap2.Hands.HandEstimator.TwoPairsRule;
import com.lsa.skienna.challenge.chap2.Hands.Card;
import com.lsa.skienna.challenge.chap2.Hands.Rank;
import com.lsa.skienna.challenge.chap2.Hands.Suit;
import com.lsa.skienna.challenge.chap2.Hands.Task;
import com.lsa.skienna.challenge.chap2.Hands.TaskReader;

public class TestHands extends TestCase{
	public void testCasesFromBook() throws Throwable{
		TaskReader reader = new TaskReader(new FileInputStream("data/pc2/hands/casesFromBook.txt"));
		Task task = reader.readNextTask();
		String result = HandEstimator.estimate(task);
		assertEquals(HandEstimator.WHITE_WINS, result);
		
		
		task = reader.readNextTask();
		result = HandEstimator.estimate(task);
		assertEquals(HandEstimator.BLACK_WINS, result);
		
		task = reader.readNextTask();
		result = HandEstimator.estimate(task);
		assertEquals(HandEstimator.BLACK_WINS, result);
		
		task = reader.readNextTask();
		result = HandEstimator.estimate(task);
		assertEquals(HandEstimator.TIE, result);
	}

	public void testHands() throws Throwable{
		TaskReader reader = new TaskReader(new FileInputStream("data/pc2/hands/hands.txt"));
		Task task;
		ScoredRule rule;
		
		task = reader.readNextTask();
		rule = new StraightFlushRule();
		applyRule(rule, task, new int[][]{{-1, StraightFlushRule.WEIGHT + Rank.SIX.getScore()}}, false, true);
		
		task = reader.readNextTask();
		rule = new FourOfAKindRule();
		applyRule(rule, task, new int[][]{{-1, FourOfAKindRule.WEIGHT + Rank.TWO.getScore()}}, false, true);
		
		task = reader.readNextTask();
		rule = new FullHouseRule();
		applyRule(rule, task, new int[][]{{-1, FullHouseRule.WEIGHT + Rank.KING.getScore()}}, false, true);
		
		task = reader.readNextTask();
		rule = new FlashRule();
		applyRule(rule, task, 
				new int[][]{
					{FlashRule.WEIGHT + Rank.JACK.getScore(), FlashRule.WEIGHT + Rank.JACK.getScore()},
					{FlashRule.WEIGHT + Rank.TEN.getScore(), FlashRule.WEIGHT + Rank.EIGHT.getScore()}
				}, 
				true, true);
		
		task = reader.readNextTask();
		rule = new StraightRule();
		applyRule(rule, task, 
				new int[][]{
					{StraightRule.WEIGHT + Rank.ACE.getScore(), StraightRule.WEIGHT + Rank.QUEEN.getScore()}
				}, 
				true, true);
		
		task = reader.readNextTask();
		rule = new ThreeOfAKindRule();
		applyRule(rule, task, 
				new int[][]{
					{ThreeOfAKindRule.WEIGHT + Rank.EIGHT.getScore(), ThreeOfAKindRule.WEIGHT + Rank.TEN.getScore()}
				}, 
				true, true);
		
		task = reader.readNextTask();
		rule = new TwoPairsRule();
		applyRule(rule, task, 
				new int[][]{
					{TwoPairsRule.WEIGHT + Rank.JACK.getScore(), TwoPairsRule.WEIGHT + Rank.TEN.getScore()}
				}, 
				true, true);
		
		task = reader.readNextTask();
		rule = new OnePairRule();
		applyRule(rule, task, 
				new int[][]{
					{OnePairRule.WEIGHT + Rank.QUEEN.getScore(), OnePairRule.WEIGHT + Rank.ACE.getScore()}
				}, 
				true, true);
		
		// inverse
		task = reader.readNextTask();
		rule = new StraightFlushRule();
		applyRule(rule, task, new int[][]{{StraightFlushRule.WEIGHT + Rank.SIX.getScore(), -1}}, true, false);
		
		task = reader.readNextTask();
		rule = new FourOfAKindRule();
		applyRule(rule, task, new int[][]{{ FourOfAKindRule.WEIGHT + Rank.TWO.getScore(), -1}}, true, false);
		
		task = reader.readNextTask();
		rule = new FullHouseRule();
		applyRule(rule, task, new int[][]{{FullHouseRule.WEIGHT + Rank.KING.getScore(), -1}}, true, false);
		
		task = reader.readNextTask();
		rule = new FlashRule();
		applyRule(rule, task, 
				new int[][]{
					{FlashRule.WEIGHT + Rank.JACK.getScore(), FlashRule.WEIGHT + Rank.JACK.getScore()},
					{FlashRule.WEIGHT + Rank.EIGHT.getScore(), FlashRule.WEIGHT + Rank.TEN.getScore()}
				}, 
				true, true);
		
		task = reader.readNextTask();
		rule = new StraightRule();
		applyRule(rule, task, 
				new int[][]{
					{StraightRule.WEIGHT + Rank.QUEEN.getScore(), StraightRule.WEIGHT + Rank.ACE.getScore()}
				}, 
				true, true);
		
		task = reader.readNextTask();
		rule = new ThreeOfAKindRule();
		applyRule(rule, task, 
				new int[][]{
					{ThreeOfAKindRule.WEIGHT + Rank.TEN.getScore(), ThreeOfAKindRule.WEIGHT + Rank.EIGHT.getScore()}
				}, 
				true, true);
		
		task = reader.readNextTask();
		rule = new TwoPairsRule();
		applyRule(rule, task, 
				new int[][]{
					{TwoPairsRule.WEIGHT + Rank.TEN.getScore(), TwoPairsRule.WEIGHT + Rank.JACK.getScore()}
				}, 
				true, true);
		
		task = reader.readNextTask();
		rule = new OnePairRule();
		applyRule(rule, task, 
				new int[][]{
					{OnePairRule.WEIGHT + Rank.ACE.getScore(), OnePairRule.WEIGHT + Rank.QUEEN.getScore()}
				}, 
				true, true);
	}
	
	void applyRule(ScoredRule rule, Task task, int[][] expectedScores,
			boolean isWhiteMatchExpected, boolean isBlackMatchExpected) {
		boolean isWhiteMatch = rule.match(task.getWhiteHand());
		HandScorer whiteScorer = rule.getHandScorer();
		assertEquals(isWhiteMatchExpected, isWhiteMatch);
		boolean isBlackMatch = rule.match(task.getBlackHand());
		HandScorer blackScorer = rule.getHandScorer();
		assertEquals(isBlackMatchExpected, isBlackMatch);
		
		int whiteScore = -1;
		int blackScore = -1;
		
		List<int[]> scoresList = new ArrayList<int[]>();
		
		do{
			whiteScore = whiteScorer.nextScore();
			blackScore = blackScorer.nextScore();
			scoresList.add(new int[]{whiteScore, blackScore});
		}while(whiteScore != -1 && blackScore != -1 && whiteScore == blackScore);

		int[][] scores = new int[scoresList.size()][];
		for (int i = 0; i < scoresList.size(); i++) {
			scores[i] = scoresList.get(i);
		}
		assertTrue(Utils.compareArrays(expectedScores, scores));
	}

	public void testEstimator() throws Throwable{
		TaskReader taskReader = new TaskReader(new FileInputStream("data/pc2/hands/hands.txt"));
		Task task;
		
		List<String> results = new ArrayList<String>();
		
		try {
			while((task = taskReader.readNextTask()) != null){
				String result = HandEstimator.estimate(task);
				results.add(result);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		String[] resultStrs = results.toArray(new String[0]);
		String blackWins = HandEstimator.BLACK_WINS;
		String whiteWins = HandEstimator.WHITE_WINS;
		assertTrue(Utils.compareArrays(resultStrs, new String[] { 
				blackWins,
				blackWins, blackWins, whiteWins, whiteWins, blackWins, whiteWins, blackWins,
				whiteWins,
				whiteWins, whiteWins, blackWins, blackWins, whiteWins, blackWins, whiteWins}));
	}
	
	public void testEstimator2() throws FileNotFoundException{
		TaskReader taskReader = new TaskReader(new FileInputStream("data/pc2/hands/hands2.txt"));
		Task task;
		
		List<String> results = new ArrayList<String>();
		
		try {
			while((task = taskReader.readNextTask()) != null){
				String result = HandEstimator.estimate(task);
				results.add(result);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		String[] resultStrs = results.toArray(new String[0]);
		String blackWins = HandEstimator.BLACK_WINS;
		String whiteWins = HandEstimator.WHITE_WINS;
		String tie = HandEstimator.TIE;
		assertTrue(Utils.compareArrays(resultStrs, new String[] {blackWins, 
				tie, tie, blackWins, whiteWins, tie}));
	}
	
	public void testGenerateHand() throws IOException{
//		List<Card> cards = HandGenerator.generateOther();
//		Collections.sort(cards, Hands.cardComparator);
//		System.out.println(HandGenerator.cards2CodeString(cards));
//		Map<Pair<RuleType, RuleType>, String> ruleTypePair2Hand = new HashMap<Pair<RuleType,RuleType>, String>();
//		Map<Pair<RuleType, RuleType>, String> ruleTypePair2Result = new HashMap<Pair<RuleType,RuleType>, String>();
		
		String blackWins = HandEstimator.BLACK_WINS;
		String whiteWins = HandEstimator.WHITE_WINS;
		String tie = HandEstimator.TIE;
		
		RuleType[] ruleTypes = RuleType.values(); 
		for (int i = ruleTypes.length - 1; i > -1; i--) {
			RuleType blackRuleType = ruleTypes[i];
			String blackHand = HandGenerator.cards2CodeString(HandGenerator.generate(blackRuleType)) + " ";
			
			for (int j = i - 1; j > -1; j--) {
				RuleType whiteRuleType = ruleTypes[j];
				blackHand = HandGenerator.cards2CodeString(HandGenerator.generate(blackRuleType)) + " ";
				String whiteHand = HandGenerator.cards2CodeString(HandGenerator.generate(whiteRuleType)) + " ";

				String taskStr = blackHand + whiteHand;
				byte[] bytes = taskStr.getBytes();
				ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
				
				TaskReader taskReader = new TaskReader(bais);
				Task task = taskReader.readNextTask();
				String result = HandEstimator.estimate(task);
				
				assertEquals(taskStr + " " + blackRuleType + " " +whiteRuleType, blackWins, result);
			}
		}
	}
	
	static enum RuleType{
		OneCardRule,
		OnePairRule,
		TwoPairsRule,
		ThreeOfAKindRule,
		StraightRule,
		FlashRule,
		FullHouseRule,
		FourOfAKindRule,
		StraightFlushRule
	}
	
	static class HandGenerator{
		static private Map<Integer, Rank> score2Rank = new HashMap<Integer, Rank>();
		static private Map<Integer, Suit> score2Suit = new HashMap<Integer, Suit>();
		
		static{
			for(Rank rank : Rank.values()){
				score2Rank.put(rank.getScore(), rank);
			}
			
			int i = 1;
			for(Suit suit : Suit.values()){
				score2Suit.put(i++, suit);
			}
		}
		
		static List<Card> generate(RuleType ruleType) {
			List<Card> result = null;
			switch (ruleType) {
			case OneCardRule:
				result =  generateOther();
				break;
			case OnePairRule:
				result =  generatePair();
				break;
			case TwoPairsRule:
				result =  generateTwoPairs();
				break;
			case ThreeOfAKindRule:
				result =  generateThreeOfAKind();
				break;
			case StraightRule:
				result =  generateStraight();
				break;
			case FlashRule:
				result =  generateFlush();
				break;
			case FullHouseRule:
				result =  generateFullHouse();
				break;
			case FourOfAKindRule:
				result =  generateFourOfAKind();
				break;
			case StraightFlushRule:
				result =  generateStraightFlush();
				break;
			}
			
//			System.out.println(cards2CodeString(result));
			return result;
		}
		
		static List<Card> generateOther(){
			List<Card> cards;
			List<Rank> ranks;
			ConsecutiveRule rule = new ConsecutiveRule(5);
			do{
				cards = new ArrayList<Card>();
				ranks = getAllRanks();
				Collections.shuffle(ranks);
				Set<Suit> suits = new HashSet<Suit>();
				
				for (int i = 0; i < 5; i++) {
					Rank rank = Utils.getRandomListElem(ranks);
					ranks.remove(rank);
					Suit suit = getRandomSuit();
					suits.add(suit);
					if (i == 4){ /// last chance
						while(suits.size() == 1){
							suit = getRandomSuit();
							suits.add(suit);
						}
					}
					cards.add(new Card(rank, suit));
				}
			}
			while(rule.match(new Hand(cards.toArray(new Card[0]))));
			
			
			return cards;
		}
		
		static List<Card> generatePair(){
			List<Card> cards = new ArrayList<Card>();
			
			List<Rank> ranks = getAllRanks();
			List<Suit> suits = getAllSuits();
			Collections.shuffle(suits);
			Collections.shuffle(ranks);
			
			int k = 0;
			int l = 0;
			
			Rank randomRank = ranks.get(l++);
			
			for (int j = 0; j < 2; j++) {
				cards.add(new Card(randomRank, suits.get(k++)));
			}
			
			for (int i = 0; i < 3; i++) {
				randomRank = ranks.get(l++);
				cards.add(new Card(randomRank, Utils.getRandomListElem(suits)));
			}
			
			return cards;
		}
		
		static List<Card> generateTwoPairs(){
			List<Card> cards = new ArrayList<Card>();
			
			List<Rank> ranks = getAllRanks();
			List<Suit> suits = getAllSuits();
			Collections.shuffle(suits);
			Collections.shuffle(ranks);
			
			int k = 0;
			int l = 0;
			
			for (int i = 0; i < 2; i++) {
				Rank randomRank = ranks.get(l++);
				
				for (int j = 0; j < 2; j++) {
					cards.add(new Card(randomRank, suits.get(k++)));
				}
			}
			
			Rank randomRank = ranks.get(l++);
			cards.add(new Card(randomRank, Utils.getRandomListElem(suits)));
			
			return cards;
		}
		
		
		static List<Card> generateThreeOfAKind(){
			List<Card> cards = new ArrayList<Card>();
			
			Rank randomRank = getRandomRank();
			List<Suit> suits = getAllSuits();
			Collections.shuffle(suits);
			
			for (int i = 0; i < 3; i++) {
				Suit suit = suits.get(i);
				cards.add(new Card(randomRank,suit));
			}
			
			
			List<Rank> ranks = getAllRanks();
			ranks.remove(randomRank);
			suits = getAllSuits();
			
			for (int i = 0; i < 2; i++) {
				randomRank = Utils.getRandomListElem(ranks);
				Suit randomSuit = Utils.getRandomListElem(suits);
				cards.add(new Card(randomRank, randomSuit));
				ranks.remove(randomRank);
				suits.remove(randomSuit);
			}
			
			return cards;
		}
		
		static List<Card> generateStraight(){
			List<Card> cards = new ArrayList<Card>();
			
			Set<Suit> suits = new HashSet<Suit>();
			
			int seedScore = Utils.random(2, 10);
			for (int i = seedScore; i < seedScore + 5; i++) {
			Suit suit = getRandomSuit();
				suits.add(suit);
				if (i - seedScore == 4){ /// last chance
					while(suits.size() == 1){
						suit = getRandomSuit();
						suits.add(suit);
					}
				}
				cards.add(new Card(score2Rank.get(i), suit));
			}
			
			return cards;
		}
		
		static List<Card> generateFlush(){
			List<Card> cards = new ArrayList<Card>();
			
			List<Card> deck = getDeck();
			Suit suit = deck.get(0).getSuit();
			int count = 5;
			for (int i = 1; i < deck.size(); i++) {
				Card card = deck.get(i);
				if (card.getSuit() != suit) continue;
				cards.add(card);
				if (--count == 0) break;
			}
			
			return cards;
		}
		
		static List<Card> generateFullHouse(){
			List<Card> cards = new ArrayList<Card>();
			
			List<Suit> suits = getAllSuits();
			Collections.shuffle(suits);
			
			Rank randomRank = getRandomRank();
			for (int i = 0; i < 3; i++) {
				Suit suit = Utils.getRandomListElem(suits);
				suits.remove(suit);
				cards.add(new Card(randomRank,suit));
			}
			
			suits = getAllSuits();
			Collection<Card> deck = getDeck();
			deck.removeAll(cards);
			Iterator<Card> iter = deck.iterator();
			while(true){
				Card next = iter.next();
				if (cards.get(0).getRank() != next.getRank()){ // avoid FOAK
					cards.add(next);
					suits.remove(next.getSuit());
					Suit suit = Utils.getRandomListElem(suits);
					cards.add(new Card(next.getRank(), suit));
					break;
				}
			}
			
			return cards;
		}
		
		static List<Card> generateFourOfAKind(){
			List<Card> cards = new ArrayList<Card>();
			
			Rank randomRank = getRandomRank();
			for(Suit suit : Suit.values()){
				cards.add(new Card(randomRank, suit));
			}
			
			Collection<Card> deck = getDeck();
			deck.removeAll(cards);
			Iterator<Card> iter = deck.iterator();
			while(true){
				Card next = iter.next();
				if (cards.get(0).getSuit() != next.getSuit()){ // avoid Straight flush
					cards.add(next);
					break;
				}
			}
			
			return cards;
		}

		static List<Card> generateStraightFlush(){
			List<Card> cards = new ArrayList<Card>();
			
			int seedScore = Utils.random(2, 10);
			Suit suit = getRandomSuit();
			for (int i = seedScore; i < seedScore + 5; i++) {
				cards.add(new Card(score2Rank.get(i), suit));
			}
			
			return cards;
		}
		
		static List<Card> getDeck(){
			List<Card> cards = new ArrayList<Card>();
			for(Suit suit : Suit.values()){
				for(Rank rank : Rank.values()){
					cards.add(new Card(rank, suit));
				}
			}
			Collections.shuffle(cards);
			return cards;
		}
		
		private static Rank getRandomRank() {
			Rank[] ranks = Rank.values();
			return ranks[Utils.random(0, ranks.length - 1)];
		}
		
		private static Suit getRandomSuit() {
			Suit[] ranks = Suit.values();
			return ranks[Utils.random(0, ranks.length - 1)];
		}
		
		private static List<Suit> getAllSuits(){
			return new ArrayList<Suit>(Arrays.asList(Suit.values()));
		}
		
		private static List<Rank> getAllRanks(){
			return new ArrayList<Rank>(Arrays.asList(Rank.values()));
		}
		
		static String cards2CodeString(List<Card> cards){
			StringBuilder sb = new StringBuilder();
			
			for (Card card : cards) {
				sb.append(card.getRank().getCode() + "" + card.getSuit().getCode()+" ");
			}
			
			return sb.toString();
		}
	}
}
